using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using T.FAS.Runtime.Base.Entity;
using T.FAS.Runtime.DataAccess.Common;
using T.FAS.Runtime.DataAccess.Kernel;
using T.FAS.Runtime.DataAccess.SPI;
using T.FAS.Runtime.DataAccess.SQLDom;
using T.FAS.Runtime.Metadata.Entity.DataModel;
using T.FAS.Runtime.Metadata.Entity;
using T.ServiceComponent.Kernel.Service;

namespace T.FAS.Runtime.DataAccess.Engine
{
    class CDODataUpdateService : IUpdateService<CompositeDataObject>
    {
        public event ExecuteDataAccessAction Execute;
        public event BuildDataAccessActionContext BuildContext;
        private readonly IDatabase _db = DatabaseFactory.CreateDefaultDatabase();
        private readonly IQueryService<CompositeDataObject> _queryService = new CDODataQueryService();
        private readonly IDACLogService _localLogService = DACLogFactory.GetDACLogService();
        private readonly OpeType _opeType = OpeType.Update;

        /// <summary>
        /// 更新CDO数据
        /// </summary>
        /// <param name="metadata">CDO元数据实体</param>
        /// <param name="newTRecord">传入的newRecord</param>
        /// <param name="extParams">额外控制参数</param>
        /// <returns>如果包含状态机，应该返回执行状态机保存前事件后的Record</returns>
        public TResult UpdateData(CompositeDataObject metadata, TRecord newTRecord, Dictionary<string, object> extParams = null)
        {
            RpcContext.Current.Increase();
            if (metadata == null)
                throw new FasException(DataAccessErrorCode.CompositeDataObject.NullValue, CommonStringClassEngine.DataModelService_MetaDataCannotBeNull);
            if (newTRecord == null)
                throw new FasException(DataAccessErrorCode.CompositeDataObject.NullValue, CommonStringClassEngine.DataModelService_TRecordCannotBeNull);
            DateTime start = DateTime.Now;
            Exception ex = null;
            bool isNeedAuth = false;
            try
            {
                //构造鉴权上下文
                if (DACControlContextHelper.IsNeedAuthAndTryGetAuthResult(metadata, out List<ColumnPermissionView> columnPermissionResult))
                {
                    isNeedAuth = true;
                }

                //初始化DB事件
                //InitDBEvent();
                RegistSQLExeSuccessdEvent(metadata);

                //初始化插件执行上下文
                InvokeContext plugInInvokeContext = PluginInit(metadata, newTRecord, null, extParams);
                plugInInvokeContext.InitAuthControl(isNeedAuth, columnPermissionResult);

                //触发入口事件
                PluginExecute(InjectionPoint.Entry);

                //old Record目前是从数据库中取出，pk由于在执行保存前事件前已经保存，所以不存在被状态机修改的情况
                string pkVal = GetCDOTRecordPrimaryKeyValue(metadata, newTRecord);
                TRecord oldTRecord = ((TCommonResult<TRecord>)_queryService.QueryData(metadata, pkVal, DACControlContextHelper.BuildDefultExtParams())).Record;

                //触发执行sql前事件，需要向插件内注入old Record，避免插件内多次查询。如果需要事务性保证，应启用DB事件。
                //优化点:根据CDO是否开启变更日志，状态机是否启用变更事件通知来决定是否进行预查询
                //_localLogService.Write(new { info = "状态机插件执行前的TRecord内容", cdo = metadata?.Code, content = newTRecord.GetTRecordDebugInfos() });
                DataAccessPluginExtParam pluginExtParam_BeforeExecuteSQL = new DataAccessPluginExtParam { OldRecord = oldTRecord, NewRecord = newTRecord };
                PluginExecute(InjectionPoint.BeforeExecuteSQL, pluginExtParam_BeforeExecuteSQL);

                //注册时间戳到线程上下文中
                var timestampStr = DACContextHelper.Regist(extParams);
                SqlExecuteControlContext controlContext = DACControlContextHelper.GetDefaultControlContext(extParams, metadata, timestampStr);
                controlContext.InitAuthControl(isNeedAuth, columnPermissionResult);

                #region 只读字段控制 -- 增加状态机只读字段控制，暂时屏蔽
                //var readonlyFields = GetReadonlyFields(plugInInvokeContext);
                //if (readonlyFields != null)
                //{
                //    //增加状态机只读字段控制，暂时屏蔽
                //    //controlContext.ReadonlyFields = readonlyFields;
                //}
                #endregion

                //_localLogService.Write(new { info = "SQL执行前的TRecord内容", cdo = metadata?.Code, content = newTRecord.GetTRecordDebugInfos() });
                SqlStatementCollection sqls = SQLBuilderFactory.CreateCDOSQLBuilder().ParseUpdateSql(metadata, newTRecord, controlContext);
                //一定要放在ParseUpdateSql之后，因为构造sql dom时会修改原始record，比如子表外键列赋值
                TRecord newTRecordClone = GetUpdatedData(metadata, newTRecord, plugInInvokeContext);

                TrimNotValidSql(sqls);
                //如果没有生成SQL语句直接返回Record本身
                if (sqls.Count == 0)
                    return ConvertDataToResult(metadata, newTRecord);

                //执行SQL
                ExecuteSQL(metadata, sqls, pkVal);

                //预先保存时间戳，防止状态机保存后事件意外修改线程上下文中的时间戳
                Timestamp currTimestamp = GetCurrDataTimestamp();

                //触发SQL执行后事件 -- 主要是状态机的保存后事件
                DataAccessPluginExtParam pluginExtParam_AfterSQLExecuted = new DataAccessPluginExtParam { NewRecord = newTRecord };
                PluginExecute(InjectionPoint.Finish, pluginExtParam_AfterSQLExecuted);

                //给Clone的Record设置预生成的时间戳，防止状态机保存后事件意外修改线程上下文中的时间戳
                SetUpdateDataTimestamp(metadata, newTRecordClone, currTimestamp);

                //清理TRecord
                newTRecordClone.CleanOut();

                //清空线程上下文注册的信息
                DACContextHelper.UnRegist();

                //返回的Data应该是执行完状态机保存前事件的Data，注意是否被AfterSave事件影响
                var result = ConvertDataToResult(metadata, newTRecordClone);

                return result;
            }
            catch (Exception e)
            {
                var msg = $"CDO:{metadata.Code}执行Update异常，msg:{e.Message}";
                _localLogService.WriteError(e, msg);
                ex = new Exception(msg, e);
                CDOMonitor.RootCaseReport(metadata, start, ex, _opeType);
                throw ex;
            }
            finally
            {
                CDOMonitor.MonitorTPS(metadata, _opeType, ex);
                CDOMonitor.MonitorTrace(metadata, start, _opeType, ex, newTRecord);
                CDOMonitor.MonitorDuration(metadata, (DateTime.Now - start).TotalMilliseconds, _opeType, ex);
            }
        }

        /// <summary>
        /// 更新CDO数据
        /// </summary>
        /// <param name="metadata">元数据</param>
        /// <param name="childNodeId">节点ID</param>
        /// <param name="newTRecord">通用结果对象</param>
        public TResult UpdateData(CompositeDataObject metadata, string childNodeId, TRecord newTRecord, Dictionary<string, object> exrParams = null)
        {
            if (metadata == null)
                throw new FasException(DataAccessErrorCode.CompositeDataObject.NullValue, CommonStringClassEngine.DataModelService_MetaDataCannotBeNull);
            if (string.IsNullOrWhiteSpace(childNodeId))
                throw new FasException(DataAccessErrorCode.CompositeDataObject.NullValue, CommonStringClassEngine.CDODataUpdateService_ChildNodeIdCannotBeNull);
            if (newTRecord == null)
                throw new FasException(DataAccessErrorCode.CompositeDataObject.NullValue, CommonStringClassEngine.DataModelService_TRecordCannotBeNull);
            var cno = metadata.CompositeNodeObjects.FirstOrDefault(i => i.ID == childNodeId);
            if (cno == null)
            {
                throw new FasException(DataAccessErrorCode.CompositeDataObject.NullValue, CommonStringClassEngine.CDOService_CannotFindCNOInstance + childNodeId);
            }
            //初始化DB事件
            //InitDBEvent();
            RegistSQLExeSuccessdEvent(metadata);

            //初始化插件
            var plugInInvokeContext = PluginInit(metadata, newTRecord, null, exrParams);

            //触发入口事件
            PluginExecute(InjectionPoint.Entry);

            var pkVal = GetCDOTRecordPrimaryKeyValue(metadata, newTRecord);
            var oldTRecord = ((TCommonResult<TRecord>)_queryService.QueryData(metadata, pkVal, DACControlContextHelper.BuildDefultExtParams())).Record;

            //注册时间戳，供变更日志以及Update语句使用
            DACContextHelper.RegistTimestamp();
            //触发sql执行成功后事件，save需要向插件内注入查询结果，提高效率，入参统一为Record类型
            DataAccessPluginExtParam pluginParam = new DataAccessPluginExtParam { OldRecord = oldTRecord, NewRecord = newTRecord, };

            //触发执行sql前事件
            //_localLogService.Write(new { info = "状态机插件执行前的TRecord内容", cdo = metadata?.Code, content = newTRecord.GetTRecordDebugInfos() });
            PluginExecute(InjectionPoint.BeforeExecuteSQL, pluginParam);

            //_localLogService.Write(new { info = "SQL执行前的TRecord内容", cdo = metadata?.Code, content = newTRecord.GetTRecordDebugInfos() });
            var sqls = SQLBuilderFactory.CreateCDOSQLBuilder().ParseUpdateSql(metadata, newTRecord);
            //一定要放在ParseUpdateSql之后，因为构造sql dom时会修改原始record，比如子表外键列赋值
            var newTRecordClone = GetUpdatedData(metadata, newTRecord, plugInInvokeContext);
            
            TrimNotValidSql(sqls);
            //如果没有生成SQL语句直接返回Record本身
            if (sqls.Count == 0)
                return ConvertDataToResult(metadata, newTRecord);

            //执行SQL
            ExecuteSQL(metadata, sqls);

            DataAccessPluginExtParam pluginParam2 = new DataAccessPluginExtParam { NewRecord = newTRecord, };

            //触发sql执行成功后事件
            PluginExecute(InjectionPoint.Finish, pluginParam2);

            SetUpdateDataTimestamp(metadata, newTRecordClone);

            //清理TRecord
            newTRecordClone.CleanOut();

            //清空时间戳
            DACContextHelper.UnRegistTimestamp();

            //返回的Data应该是执行完状态机保存前事件的Data
            return ConvertDataToResult(metadata, newTRecordClone);
        }

        /// <summary>
        /// 更新CDO数据
        /// </summary>
        /// <param name="metadata"></param>
        /// <param name="newTRecords"></param>
        /// <param name="exrParams"></param>
        /// <returns>如果包含状态机，应该返回执行状态机保存前事件后的Record</returns>
        public BatchUpdateResult BatchUpdateData(CompositeDataObject metadata, List<TRecord> newTRecords, Dictionary<string, object> exrParams = null)
        {
            RpcContext.Current.Increase();
            if (metadata == null || string.IsNullOrEmpty(metadata.ID) || string.IsNullOrEmpty(metadata.Code))
                throw new FasException(DataAccessErrorCode.CompositeDataObject.NullValue, CommonStringClassEngine.CDOService_CDOCannotBeNull);

            BatchUpdateResult batchUpdateResult = new BatchUpdateResult
            {
                CDOID = metadata.ID,
                CDOCode = metadata.Code,
                TotalCount = newTRecords.Count,
                BatchUpdateItems = new List<BatchUpdateItem>(),
            };

            DateTime start = DateTime.Now;
            Exception ex = null;

            try
            {
                if (exrParams == null) exrParams = new Dictionary<string, object>();
                exrParams[DACConst.CDO.IsBatchUpdate] = true;
                int successCount = 0;
                if (newTRecords != null && newTRecords.Count > 0)
                {
                    foreach (var record in newTRecords)
                    {
                        BatchUpdateItem batchUpdateItem = new BatchUpdateItem();
                        try
                        {
                            batchUpdateItem.PKVal = record.GetPKValue(metadata);
                            UpdateData(metadata, record, exrParams);
                            batchUpdateItem.Result = true;
                            successCount++;
                        }
                        catch (Exception e)
                        {
                            batchUpdateItem.Result = false;
                            batchUpdateItem.ErrorMsg = e.GetInnermostExceptionMsg();
                            batchUpdateItem.ErrorStack = e.GetFullStackTrace();
                        }
                        batchUpdateResult.BatchUpdateItems.Add(batchUpdateItem);
                    }
                }
                batchUpdateResult.SuccessCount = successCount;
                batchUpdateResult.FailCount = newTRecords.Count - successCount;
                if (batchUpdateResult.FailCount == 0)
                    batchUpdateResult.Result = true;

                return batchUpdateResult;
            }
            catch (Exception e)
            {
                ex = new Exception($"{metadata?.Code}批量更新失败", e);
                CDOMonitor.RootCaseReport(metadata, start, ex, _opeType);
                throw ex;
            }
            finally
            {
                var pkVals = batchUpdateResult.BatchUpdateItems?.Select(i => i.PKVal);
                var pkVal = pkVals == null ? null : string.Join(",", pkVals);
                CDOMonitor.MonitorTPS(metadata, _opeType, ex);
                CDOMonitor.MonitorTrace(metadata, start, _opeType, ex, null, pkVal);
                CDOMonitor.MonitorDuration(metadata, (DateTime.Now - start).TotalMilliseconds, _opeType, ex);
            }
        }

        #region CDO插件执行相关

        /// <summary>
        /// 获取保存前事件执行完毕后的TRecord
        /// </summary>
        /// <param name="metadata"></param>
        /// <param name="data"></param>
        /// <param name="plugInInvokeContext"></param>
        /// <returns></returns>
        private TRecord GetUpdatedData(CompositeDataObject metadata, TRecord data, InvokeContext plugInInvokeContext)
        {
            //从外部拿到执行完保存前事件的数据。时间戳字段
            var beforeSavedData = plugInInvokeContext.GetData(PluginConst.StateMachine.Code, PluginConst.StateMachine.BeforeSavedData) as TRecord;
            TRecord newData;
            //如果CDO不包含状态机，则返回data本身
            if (beforeSavedData == null)
                newData = data;
            else
            {
                //Clone或者别的方式获取拷贝, 防止AfterSave事件操作修改数据
                //Bug:TRecord.Clone()目前是浅拷贝，且Remove掉了主键
                newData = beforeSavedData.CloneWithoutChangePkValue() as TRecord;
                //兼容处理，如果Copy时丢掉主键，则重新赋值
                newData.SetValue(beforeSavedData.PKColumnName, beforeSavedData.GetValue(beforeSavedData.PKColumnName));
            }
            if (newData == null)
                throw new FasException(DataAccessErrorCode.CompositeDataObject.Error, string.Format(CommonStringClassEngine.CDODataUpdateService_GetUpdatedData,metadata.Code));
            return newData;
        }

        /// <summary>
        /// 获取状态机设置的只读字段
        /// </summary>
        /// <param name="plugInInvokeContext"></param>
        /// <returns></returns>
        private List<string> GetReadonlyFields(InvokeContext plugInInvokeContext)
        {
            var fieldNames = plugInInvokeContext.GetData(PluginConst.StateMachine.Code, PluginConst.StateMachine.CurrentRuleReadonlyFields) as List<string>;
            return fieldNames;
        }

        private TRecord SetUpdateDataTimestamp(CompositeDataObject metadata, TRecord data)
        {
            var timestampColumnName = metadata.RootCompositeNodeObject?.GetTimestampElement()?.DataColumn?.ColumnName;
            if (!string.IsNullOrEmpty(timestampColumnName))
            {
                //throw new FasException(DataAccessErrorCode.CompositeDataObject.NoTimestampField, $"CDO:{metadata.Code}主节点找不到时间戳字段");
                DateTime time = DACContextHelper.GetTimestamp();
                data.SetValue(timestampColumnName, new Timestamp { Data = time.ToString(Timestamp.DefaultTimeFormat), Value = time });
            }
            return data;
        }

        private TRecord SetUpdateDataTimestamp(CompositeDataObject metadata, TRecord data, Timestamp timestamp)
        {
            var timestampColumnName = metadata.RootCompositeNodeObject?.GetTimestampElement()?.DataColumn?.ColumnName;
            if (!string.IsNullOrEmpty(timestampColumnName))
            {
                data.SetValue(timestampColumnName, timestamp);
            }
            return data;
        }


        private Timestamp GetCurrDataTimestamp()
        {
            DateTime time = DACContextHelper.GetTimestamp();
            return new Timestamp { Data = time.ToString(Timestamp.DefaultTimeFormat), Value = time };
        }

        private void InitDBEvent()
        {
            _db.BeforeSQLExecute += () => this.Execute(InjectionMethodType.Create, InjectionPoint.BeforeExecuteSQL);
            _db.ExecuteSQLSuccessed += result => this.Execute(InjectionMethodType.Create, InjectionPoint.ExecuteSQLSuccess);
        }

        private void RegistSQLExeSuccessdEvent(CompositeDataObject compositeDataObject)
        {
            var rootNode = compositeDataObject.RootCompositeNodeObject;
            _db.ExecuteSQLSuccessed += (result) =>
            {
                if (result.SQLExecuteContext.SqlBuildingInfo.CurrentNode.ID != rootNode.ID)  //只判断主节点数据影响行数
                    return;
                if (result.AffectedRows == 0)
                {
                    StringBuilder sb = new StringBuilder();
                    var paras = result.SQLExecuteContext.Parameters;
                    if (paras != null && paras.Count > 0)
                    {
                        foreach (var para in paras)
                        {
                            if (para == null)
                                continue;
                            sb.AppendLine($"ParaName:{para.ParameterName}, ParaValue:{para.Value?.ToString()}");
                        }
                    }
                    throw new FasException(DataAccessErrorCode.CompositeDataObject.AffectedRowsError, string.Format(CommonStringClassEngine.CDODataUpdateService_RegistSQLExeSuccessdEvent,compositeDataObject?.Code));
                }
            };
        }

        /// <summary>
        /// 初始化插件调用上下文
        /// </summary>
        /// <param name="compositeDataObject"></param>
        /// <param name="record"></param>
        /// <param name="cnoId"></param>
        /// <param name="exrParams"></param>
        /// <returns></returns>
        public InvokeContext PluginInit(CompositeDataObject compositeDataObject, TRecord record, string cnoId = null, Dictionary<string, object> exrParams = null)
        {
            InvokeContext invokeContext = DataAccessPluginExecutor.BuildInvokeContext(DataModelConst.CDOMetaType, compositeDataObject, cnoId, record);
            invokeContext.ExtProperties = exrParams;
            this.BuildContext(invokeContext);
            invokeContext.InitExtProps<bool>(exrParams, DACConst.CDO.IsBatchUpdate);
            return invokeContext;
        }

        private void PluginExecute(InjectionPoint injectionPoint, DataAccessPluginExtParam extData = null)
        {
            /************统一数据访问插件执行点 Start************/
            try
            {
                //var invokeContext = DataAccessPluginExecutor.BuildInvokeContext(DataModelConst.CDOMetaType, compositeDataObject, cnoId, record);
                var actionResult = this.Execute(InjectionMethodType.Update, injectionPoint, extData);
                DataAccessPluginExecutor.DefaultHandleActionResult(actionResult);
            }
            catch (Exception e)
            {
                throw new FasException(DataAccessErrorCode.PlugIn.Error, string.Format(CommonStringClassEngine.DataModelService_PluginExecFail, e.Message), e);
            }
        }

        #endregion

        #region Private Funcs
        /// <summary>
        /// 取出TRecord中的主节点的主键值
        /// </summary>
        /// <param name="compositeDataObject"></param>
        /// <param name="record"></param>
        /// <returns></returns>
        private string GetCDOTRecordPrimaryKeyValue(CompositeDataObject compositeDataObject, TRecord record)
        {
            try
            {
                var pkCol = compositeDataObject.RootCompositeNodeObject?.DataObject.Columns.FirstOrDefault(c => c.IsPkColumn);
                return record.GetValue(pkCol.ColumnName).ToString();
            }
            catch (Exception e)
            {
                throw new Exception(string.Format(CommonStringClassEngine.DataModelService_GetRecordPkValueFail), e);
            }
        }

        private static TResult ConvertDataToResult(CompositeDataObject metadata, TRecord data)
        {
            var result = new TCommonResult<TRecord>();
            result.Record = data;
            TResult tResult = TResultBuilder.BuildTResult(result, metadata.BuildSchema());
            return tResult;
        }

        /// <summary>
        /// 统一执行SQL，提供友好性报错提示
        /// </summary>
        /// <param name="metaData"></param>
        /// <param name="sqls"></param>
        /// <returns></returns>
        private void ExecuteSQL(CompositeDataObject metaData, SqlStatementCollection sqls, string billID = null)
        {
            try
            {
                _db.ExecuteSQLWithTransaction(sqls);
            }
            catch (Exception e)
            {
                throw new FasException(DataAccessErrorCode.CompositeDataObject.Error, string.Format(CommonStringClassEngine.CDODataUpdateService_ExecuteSQL,metaData?.Code,billID), e);
            }
        }

        /// <summary>
        /// 修剪掉update fields为空的update语句，认为其不需要更新。后续需要从Update Build Strategy内部判断
        /// </summary>
        /// <param name="sqls"></param>
        private void TrimNotValidSql(SqlStatementCollection sqls)
        {
            if (sqls == null || sqls.Count == 0) return;

            List<SqlStatement> needTrimSqls = new List<SqlStatement>();
            foreach (SqlStatement sql in sqls)
            {
                if (sql is UpdateSqlStatement updateSql && updateSql.UpdateFields.ChildCollection.Count == 0)
                {
                    needTrimSqls.Add(sql);
                }
            }
            if (needTrimSqls.Count > 0)
            {
                foreach (var trimSql in needTrimSqls)
                {
                    sqls.Remove(trimSql);
                }
            }
        }
        #endregion
    }
}